package org.synclib.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.synclib.util.StringUtils;

public abstract class SyncableObject {

	private final String uniqueId;

	private Map<String, Object> properties = Collections.emptyMap();

	private long lastModified;

	public SyncableObject() {
		this(UUID.randomUUID().toString(), System.currentTimeMillis());
	}

	/**
	 * Pass <tt>null</tt> as the uuid if you wish to provide your own
	 * implementation of <tt>getUniqueId()</tt>
	 * @param uuid
	 * @param lastModified
	 */
	public SyncableObject(String uuid, long lastModified) {
		this.uniqueId = uuid;
		this.lastModified = lastModified;
	}

	/**
	 * Subclasses can override if they want to provide a separate method for
	 * comparing two objects. For example, a Book object could use an internal
	 * ISBN field to distinguish itself.
	 * 
	 * @return This SyncableObject's unique ID.
	 */
	public String getUniqueId() {
		return uniqueId;
	}

	public void initializeProperty(String propertyName, Object value) {
		if (properties.isEmpty()) {
			properties = new HashMap<String, Object>();
		}
		properties.put(propertyName, value);
	}

	public void setProperty(String propertyName, Object value) {
		initializeProperty(propertyName, value);
		update();
	}

	protected void update() {
		this.lastModified = System.currentTimeMillis();
	}

	public long getLastModified() {
		return this.lastModified;
	}

	public Object getProperty(String propertyName) {
		return properties.get(propertyName);
	}

	/**
	 * Subclasses should implement.
	 * 
	 * @return A list of field names that should be examined when comparing this
	 *         SyncableObject with a remote version.
	 */
	public abstract List<String> getSyncableProperties();

	public void applyChanges(List<Change<?>> changes) {
		for (Change<?> c : changes) {
			if (StringUtils.equals(c.getObjectUUID(), getUniqueId())) {
				setProperty(c.getPropertyName(), c.getNewValue());
			}
		}
	}

	/**
	 * Compares this SyncableObject with its remote counterpart. Returns a list
	 * of Change objects that can be applied to the local version to bring it
	 * up-to-date with the remote version. After applying the changes to the
	 * local version, the remote version can be replaced, and both will be in
	 * sync.
	 * 
	 * @param remote
	 *            The remote SyncableObject that this SyncableObject should be
	 *            compared to.
	 * @param force
	 *            Whether or not the comparison should occur even if the 'last
	 *            modified' dates of the objects are the same.
	 * @return A list of Change objects. These objects can be applied to this
	 *         SyncableObject to leave it synchronized with the remote version.
	 *         (Note that these changes either need to be reversed and applied
	 *         to the remote version, or the remote version needs to be replaced
	 *         with the local copy to bring both versions up to speed.)
	 */
	public List<Change<?>> compareWithRemote(SyncableObject remote,
			boolean force) {
		if (!StringUtils.equals(getUniqueId(), remote.getUniqueId()))
			return Collections.emptyList();
		List<Change<?>> changes = new ArrayList<Change<?>>();
		boolean areSame = getLastModified() == remote.getLastModified();
		if (areSame && !force)
			return Collections.emptyList();
		boolean localCopyIsNewer = getLastModified() > remote.getLastModified();
		for (String propertyName : properties.keySet()) {
			Object localObject = getProperty(propertyName);
			Object remoteObject = remote.getProperty(propertyName);
			if (localObject.equals(remoteObject)) {
				continue;
			} else {
				Change<?> c = null;
				if (localCopyIsNewer) {
					c = new ValueChange<Object>(getUniqueId(), propertyName,
							remoteObject, localObject);
				} else {
					c = new ValueChange<Object>(getUniqueId(), propertyName,
							localObject, remoteObject);
				}
				changes.add(c);
			}
		}
		return changes;
	}

	/**
	 * Equivalent to calling <tt>compareWithRemote(remote, <b>false</b>)</tt>
	 */
	public List<Change<?>> compareWithRemote(SyncableObject remote) {
		return compareWithRemote(remote, false);
	}
}
